{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/pinecone-io/examples/blob/master/learn/generation/langchain/rag-chatbot.ipynb) [![Open nbviewer](https://raw.githubusercontent.com/pinecone-io/examples/master/assets/nbviewer-shield.svg)](https://nbviewer.org/github/pinecone-io/examples/blob/master/learn/generation/langchain/rag-chatbot.ipynb)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Building RAG Chatbots with LangChain"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example, we'll work on building an AI chatbot from start-to-finish. We will be using LangChain, OpenAI, and Pinecone vector DB, to build a chatbot capable of learning from the external world using **R**etrieval **A**ugmented **G**eneration (RAG).\n",
    "\n",
    "We will be using a dataset sourced from the Llama 2 ArXiv paper and other related papers to help our chatbot answer questions about the latest and greatest in the world of GenAI.\n",
    "\n",
    "By the end of the example we'll have a functioning chatbot and RAG pipeline that can hold a conversation and provide informative responses based on a knowledge base.\n",
    "\n",
    "### Before you begin\n",
    "\n",
    "You'll need to get an [OpenAI API key](https://platform.openai.com/account/api-keys) and [Pinecone API key](https://app.pinecone.io)."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Prerequisites"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Before we start building our chatbot, we need to install some Python libraries. Here's a brief overview of what each library does:\n",
    "\n",
    "- **langchain**: This is a library for GenAI. We'll use it to chain together different language models and components for our chatbot.\n",
    "- **openai**: This is the official OpenAI Python client. We'll use it to interact with the OpenAI API and generate responses for our chatbot.\n",
    "- **datasets**: This library provides a vast array of datasets for machine learning. We'll use it to load our knowledge base for the chatbot.\n",
    "- **pinecone-client**: This is the official Pinecone Python client. We'll use it to interact with the Pinecone API and store our chatbot's knowledge base in a vector database.\n",
    "\n",
    "You can install these libraries using pip like so:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install -qU \\\n",
    "    langchain==0.0.354 \\\n",
    "    openai==1.6.1 \\\n",
    "    datasets==2.10.1 \\\n",
    "    pinecone-client==3.1.0 \\\n",
    "    tiktoken==0.5.2"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Building a Chatbot (no RAG)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will be relying heavily on the LangChain library to bring together the different components needed for our chatbot. To begin, we'll create a simple chatbot without any retrieval augmentation. We do this by initializing a `ChatOpenAI` object. For this we do need an [OpenAI API key](https://platform.openai.com/account/api-keys)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from langchain.chat_models import ChatOpenAI\n",
    "\n",
    "os.environ[\"OPENAI_API_KEY\"] = os.getenv(\"OPENAI_API_KEY\") or \"YOUR_API_KEY\"\n",
    "\n",
    "chat = ChatOpenAI(\n",
    "    openai_api_key=os.environ[\"OPENAI_API_KEY\"],\n",
    "    model='gpt-3.5-turbo'\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Chats with OpenAI's `gpt-3.5-turbo` and `gpt-4` chat models are typically structured (in plain text) like this:\n",
    "\n",
    "```\n",
    "System: You are a helpful assistant.\n",
    "\n",
    "User: Hi AI, how are you today?\n",
    "\n",
    "Assistant: I'm great thank you. How can I help you?\n",
    "\n",
    "User: I'd like to understand string theory.\n",
    "\n",
    "Assistant:\n",
    "```\n",
    "\n",
    "The final `\"Assistant:\"` without a response is what would prompt the model to continue the conversation. In the official OpenAI `ChatCompletion` endpoint these would be passed to the model in a format like:\n",
    "\n",
    "```python\n",
    "[\n",
    "    {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n",
    "    {\"role\": \"user\", \"content\": \"Hi AI, how are you today?\"},\n",
    "    {\"role\": \"assistant\", \"content\": \"I'm great thank you. How can I help you?\"}\n",
    "    {\"role\": \"user\", \"content\": \"I'd like to understand string theory.\"}\n",
    "]\n",
    "```\n",
    "\n",
    "In LangChain there is a slightly different format. We use three _message_ objects like so:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.schema import (\n",
    "    SystemMessage,\n",
    "    HumanMessage,\n",
    "    AIMessage\n",
    ")\n",
    "\n",
    "messages = [\n",
    "    SystemMessage(content=\"You are a helpful assistant.\"),\n",
    "    HumanMessage(content=\"Hi AI, how are you today?\"),\n",
    "    AIMessage(content=\"I'm great thank you. How can I help you?\"),\n",
    "    HumanMessage(content=\"I'd like to understand string theory.\")\n",
    "]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The format is very similar, we're just swapped the role of `\"user\"` for `HumanMessage`, and the role of `\"assistant\"` for `AIMessage`.\n",
    "\n",
    "We generate the next response from the AI by passing these messages to the `ChatOpenAI` object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='String theory is a theoretical framework in physics that aims to explain the fundamental nature of particles and their interactions. According to string theory, the basic building blocks of the universe are not particles, but tiny vibrating strings.\\n\\nHere are some key points to help you understand string theory:\\n\\n1. Fundamental Particles: In traditional particle physics, elementary particles are considered point-like entities. In string theory, they are replaced by tiny, one-dimensional strings. These strings can vibrate in different modes, producing different particle properties such as mass and charge.\\n\\n2. Extra Dimensions: String theory suggests that there may be extra dimensions beyond the three spatial dimensions (length, width, and height) that we experience. These additional dimensions are believed to be curled up or compactified at incredibly small scales, making them invisible to our current observations.\\n\\n3. Unification of Forces: String theory has the potential to unify the four fundamental forces in physics: gravity, electromagnetism, and the strong and weak nuclear forces. By considering the vibrational patterns of strings, it becomes possible to describe these forces as different manifestations of a single underlying theory.\\n\\n4. Multiverse and String Landscape: String theory proposes the existence of multiple possible solutions or configurations, known as the \"string landscape.\" Each configuration corresponds to a possible universe with its own physical laws and properties. This idea leads to the concept of a multiverse, where our universe is just one among many.\\n\\n5. Challenges and Research: Despite its promising potential, string theory is still a work in progress and faces several challenges. For example, it has yet to make testable predictions that can be experimentally verified. Researchers continue to explore and refine the theory through mathematical calculations and simulations.\\n\\nUnderstanding string theory requires a strong background in physics and mathematics, as it is a complex and mathematically rigorous subject. It is an active area of research, and scientists are continually working to uncover its implications and validate its predictions.\\n\\nPlease note that this is a simplified overview of string theory, and there are many more intricate details and concepts involved.')"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "res = chat(messages)\n",
    "res"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In response we get another AI message object. We can print it more clearly like so:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "String theory is a theoretical framework in physics that aims to explain the fundamental nature of particles and their interactions. According to string theory, the basic building blocks of the universe are not particles, but tiny vibrating strings.\n",
      "\n",
      "Here are some key points to help you understand string theory:\n",
      "\n",
      "1. Fundamental Particles: In traditional particle physics, elementary particles are considered point-like entities. In string theory, they are replaced by tiny, one-dimensional strings. These strings can vibrate in different modes, producing different particle properties such as mass and charge.\n",
      "\n",
      "2. Extra Dimensions: String theory suggests that there may be extra dimensions beyond the three spatial dimensions (length, width, and height) that we experience. These additional dimensions are believed to be curled up or compactified at incredibly small scales, making them invisible to our current observations.\n",
      "\n",
      "3. Unification of Forces: String theory has the potential to unify the four fundamental forces in physics: gravity, electromagnetism, and the strong and weak nuclear forces. By considering the vibrational patterns of strings, it becomes possible to describe these forces as different manifestations of a single underlying theory.\n",
      "\n",
      "4. Multiverse and String Landscape: String theory proposes the existence of multiple possible solutions or configurations, known as the \"string landscape.\" Each configuration corresponds to a possible universe with its own physical laws and properties. This idea leads to the concept of a multiverse, where our universe is just one among many.\n",
      "\n",
      "5. Challenges and Research: Despite its promising potential, string theory is still a work in progress and faces several challenges. For example, it has yet to make testable predictions that can be experimentally verified. Researchers continue to explore and refine the theory through mathematical calculations and simulations.\n",
      "\n",
      "Understanding string theory requires a strong background in physics and mathematics, as it is a complex and mathematically rigorous subject. It is an active area of research, and scientists are continually working to uncover its implications and validate its predictions.\n",
      "\n",
      "Please note that this is a simplified overview of string theory, and there are many more intricate details and concepts involved.\n"
     ]
    }
   ],
   "source": [
    "print(res.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Because `res` is just another `AIMessage` object, we can append it to `messages`, add another `HumanMessage`, and generate the next response in the conversation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Physicists believe that string theory has the potential to produce a unified theory because it incorporates all the fundamental forces of nature within a single framework. The four fundamental forces in physics are gravity, electromagnetism, and the strong and weak nuclear forces. \n",
      "\n",
      "In the standard model of particle physics, these forces are described by different theories that are not fully compatible with each other. For example, gravity is described by general relativity, which explains the behavior of massive objects on large scales, while the other forces are described by quantum field theories.\n",
      "\n",
      "String theory, on the other hand, provides a framework that can potentially reconcile and unify these forces. The vibrations of the tiny strings in string theory can give rise to different types of particles, and the way these particles interact can reproduce the behavior of the various fundamental forces. This means that, in principle, all the forces of nature could be described by a single underlying theory.\n",
      "\n",
      "Additionally, string theory incorporates gravity naturally, unlike the other forces, which have been challenging to reconcile with gravity in other theoretical frameworks. By including gravity within the framework of string theory, physicists hope to achieve a consistent and unified description of all the fundamental forces.\n",
      "\n",
      "However, it is important to note that achieving a complete and fully validated unified theory based on string theory is still a work in progress. Many challenges and open questions remain, and further research and experimentation are needed to test and refine the theory.\n"
     ]
    }
   ],
   "source": [
    "# add latest AI response to messages\n",
    "messages.append(res)\n",
    "\n",
    "# now create a new user prompt\n",
    "prompt = HumanMessage(\n",
    "    content=\"Why do physicists believe it can produce a 'unified theory'?\"\n",
    ")\n",
    "# add to messages\n",
    "messages.append(prompt)\n",
    "\n",
    "# send to chat-gpt\n",
    "res = chat(messages)\n",
    "\n",
    "print(res.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Dealing with Hallucinations"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We have our chatbot, but as mentioned — the knowledge of LLMs can be limited. The reason for this is that LLMs learn all they know during training. An LLM essentially compresses the \"world\" as seen in the training data into the internal parameters of the model. We call this knowledge the _parametric knowledge_ of the model.\n",
    "\n",
    "By default, LLMs have no access to the external world.\n",
    "\n",
    "The result of this is very clear when we ask LLMs about more recent information, like about the new (and very popular) Llama 2 LLM."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# add latest AI response to messages\n",
    "messages.append(res)\n",
    "\n",
    "# now create a new user prompt\n",
    "prompt = HumanMessage(\n",
    "    content=\"What is so special about Llama 2?\"\n",
    ")\n",
    "# add to messages\n",
    "messages.append(prompt)\n",
    "\n",
    "# send to OpenAI\n",
    "res = chat(messages)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "I'm sorry, but I don't have any specific information about \"Llama 2.\" It could be a reference to something specific that I am not aware of. Could you please provide more context or clarify your question?\n"
     ]
    }
   ],
   "source": [
    "print(res.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our chatbot can no longer help us, it doesn't contain the information we need to answer the question. It was very clear from this answer that the LLM doesn't know the informaiton, but sometimes an LLM may respond like it _does_ know the answer — and this can be very hard to detect.\n",
    "\n",
    "OpenAI have since adjusted the behavior for this particular example as we can see below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "# add latest AI response to messages\n",
    "messages.append(res)\n",
    "\n",
    "# now create a new user prompt\n",
    "prompt = HumanMessage(\n",
    "    content=\"Can you tell me about the LLMChain in LangChain?\"\n",
    ")\n",
    "# add to messages\n",
    "messages.append(prompt)\n",
    "\n",
    "# send to OpenAI\n",
    "res = chat(messages)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "I apologize, but I couldn't find any specific information about \"LLMChain\" or \"LangChain.\" It's possible that these terms are specific to a particular context or project that I am not familiar with. If you can provide more details or clarify your question, I'll do my best to assist you.\n"
     ]
    }
   ],
   "source": [
    "print(res.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There is another way of feeding knowledge into LLMs. It is called _source knowledge_ and it refers to any information fed into the LLM via the prompt. We can try that with the LLMChain question. We can take a description of this object from the LangChain documentation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "llmchain_information = [\n",
    "    \"A LLMChain is the most common type of chain. It consists of a PromptTemplate, a model (either an LLM or a ChatModel), and an optional output parser. This chain takes multiple input variables, uses the PromptTemplate to format them into a prompt. It then passes that to the model. Finally, it uses the OutputParser (if provided) to parse the output of the LLM into a final format.\",\n",
    "    \"Chains is an incredibly generic concept which returns to a sequence of modular components (or other chains) combined in a particular way to accomplish a common use case.\",\n",
    "    \"LangChain is a framework for developing applications powered by language models. We believe that the most powerful and differentiated applications will not only call out to a language model via an api, but will also: (1) Be data-aware: connect a language model to other sources of data, (2) Be agentic: Allow a language model to interact with its environment. As such, the LangChain framework is designed with the objective in mind to enable those types of applications.\"\n",
    "]\n",
    "\n",
    "source_knowledge = \"\\n\".join(llmchain_information)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can feed this additional knowledge into our prompt with some instructions telling the LLM how we'd like it to use this information alongside our original query."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "query = \"Can you tell me about the LLMChain in LangChain?\"\n",
    "\n",
    "augmented_prompt = f\"\"\"Using the contexts below, answer the query.\n",
    "\n",
    "Contexts:\n",
    "{source_knowledge}\n",
    "\n",
    "Query: {query}\"\"\""
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we feed this into our chatbot as we were before."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "# create a new user prompt\n",
    "prompt = HumanMessage(\n",
    "    content=augmented_prompt\n",
    ")\n",
    "# add to messages\n",
    "messages.append(prompt)\n",
    "\n",
    "# send to OpenAI\n",
    "res = chat(messages)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The LLMChain is a specific type of chain within the LangChain framework. It is designed to facilitate the development of applications powered by language models. \n",
      "\n",
      "A LLMChain consists of three main components: a PromptTemplate, a language model (either an LLM or a ChatModel), and an optional output parser. \n",
      "\n",
      "The PromptTemplate takes multiple input variables and formats them into a prompt. This prompt is then passed to the language model, which generates a response based on the provided input. \n",
      "\n",
      "The optional output parser, if provided, is used to parse the output of the language model into a final format that suits the specific application's needs. This allows for customization and flexibility in processing the model's output.\n",
      "\n",
      "The LangChain framework, of which the LLMChain is a part, aims to enable powerful and differentiated applications by not only connecting to a language model via an API but also by being data-aware and agentic. Being data-aware means that the framework allows for integration with other sources of data, enhancing the capabilities of the language model. Being agentic means that the framework enables the language model to interact with its environment, making it more dynamic and adaptable.\n",
      "\n",
      "In summary, the LLMChain is a key component within the LangChain framework, providing a structure for integrating language models into applications and enabling data-aware and agentic functionality.\n"
     ]
    }
   ],
   "source": [
    "print(res.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The quality of this answer is phenomenal. This is made possible thanks to the idea of augmented our query with external knowledge (source knowledge). There's just one problem — how do we get this information in the first place?\n",
    "\n",
    "We learned in the previous chapters about Pinecone and vector databases. Well, they can help us here too. But first, we'll need a dataset."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importing the Data"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this task, we will be importing our data. We will be using the Hugging Face Datasets library to load our data. Specifically, we will be using the `\"jamescalam/llama-2-arxiv-papers\"` dataset. This dataset contains a collection of ArXiv papers which will serve as the external knowledge base for our chatbot."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Dataset({\n",
       "    features: ['doi', 'chunk-id', 'chunk', 'id', 'title', 'summary', 'source', 'authors', 'categories', 'comment', 'journal_ref', 'primary_category', 'published', 'updated', 'references'],\n",
       "    num_rows: 4838\n",
       "})"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from datasets import load_dataset\n",
    "\n",
    "dataset = load_dataset(\n",
    "    \"jamescalam/llama-2-arxiv-papers-chunked\",\n",
    "    split=\"train\"\n",
    ")\n",
    "\n",
    "dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'doi': '1102.0183',\n",
       " 'chunk-id': '0',\n",
       " 'chunk': 'High-Performance Neural Networks\\nfor Visual Object Classi\\x0ccation\\nDan C. Cire\\x18 san, Ueli Meier, Jonathan Masci,\\nLuca M. Gambardella and J\\x7f urgen Schmidhuber\\nTechnical Report No. IDSIA-01-11\\nJanuary 2011\\nIDSIA / USI-SUPSI\\nDalle Molle Institute for Arti\\x0ccial Intelligence\\nGalleria 2, 6928 Manno, Switzerland\\nIDSIA is a joint institute of both University of Lugano (USI) and University of Applied Sciences of Southern Switzerland (SUPSI),\\nand was founded in 1988 by the Dalle Molle Foundation which promoted quality of life.\\nThis work was partially supported by the Swiss Commission for Technology and Innovation (CTI), Project n. 9688.1 IFF:\\nIntelligent Fill in Form.arXiv:1102.0183v1  [cs.AI]  1 Feb 2011\\nTechnical Report No. IDSIA-01-11 1\\nHigh-Performance Neural Networks\\nfor Visual Object Classi\\x0ccation\\nDan C. Cire\\x18 san, Ueli Meier, Jonathan Masci,\\nLuca M. Gambardella and J\\x7f urgen Schmidhuber\\nJanuary 2011\\nAbstract\\nWe present a fast, fully parameterizable GPU implementation of Convolutional Neural\\nNetwork variants. Our feature extractors are neither carefully designed nor pre-wired, but',\n",
       " 'id': '1102.0183',\n",
       " 'title': 'High-Performance Neural Networks for Visual Object Classification',\n",
       " 'summary': 'We present a fast, fully parameterizable GPU implementation of Convolutional\\nNeural Network variants. Our feature extractors are neither carefully designed\\nnor pre-wired, but rather learned in a supervised way. Our deep hierarchical\\narchitectures achieve the best published results on benchmarks for object\\nclassification (NORB, CIFAR10) and handwritten digit recognition (MNIST), with\\nerror rates of 2.53%, 19.51%, 0.35%, respectively. Deep nets trained by simple\\nback-propagation perform better than more shallow ones. Learning is\\nsurprisingly rapid. NORB is completely trained within five epochs. Test error\\nrates on MNIST drop to 2.42%, 0.97% and 0.48% after 1, 3 and 17 epochs,\\nrespectively.',\n",
       " 'source': 'http://arxiv.org/pdf/1102.0183',\n",
       " 'authors': ['Dan C. Cireşan',\n",
       "  'Ueli Meier',\n",
       "  'Jonathan Masci',\n",
       "  'Luca M. Gambardella',\n",
       "  'Jürgen Schmidhuber'],\n",
       " 'categories': ['cs.AI', 'cs.NE'],\n",
       " 'comment': '12 pages, 2 figures, 5 tables',\n",
       " 'journal_ref': None,\n",
       " 'primary_category': 'cs.AI',\n",
       " 'published': '20110201',\n",
       " 'updated': '20110201',\n",
       " 'references': []}"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dataset[0]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Dataset Overview\n",
    "\n",
    "The dataset we are using is sourced from the Llama 2 ArXiv papers. It is a collection of academic papers from ArXiv, a repository of electronic preprints approved for publication after moderation. Each entry in the dataset represents a \"chunk\" of text from these papers.\n",
    "\n",
    "Because most **L**arge **L**anguage **M**odels (LLMs) only contain knowledge of the world as it was during training, they cannot answer our questions about Llama 2 — at least not without this data."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Task 4: Building the Knowledge Base"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now have a dataset that can serve as our chatbot knowledge base. Our next task is to transform that dataset into the knowledge base that our chatbot can use. To do this we must use an embedding model and vector database.\n",
    "\n",
    "We begin by initializing our connection to Pinecone, this requires a [free API key](https://app.pinecone.io)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pinecone import Pinecone\n",
    "\n",
    "# initialize connection to pinecone (get API key at app.pinecone.io)\n",
    "api_key = os.getenv(\"PINECONE_API_KEY\") or \"YOUR_API_KEY\"\n",
    "\n",
    "# configure client\n",
    "pc = Pinecone(api_key=api_key)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we setup our index specification, this allows us to define the cloud provider and region where we want to deploy our index. You can find a list of all [available providers and regions here](https://docs.pinecone.io/docs/projects)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pinecone import ServerlessSpec\n",
    "\n",
    "spec = ServerlessSpec(\n",
    "    cloud=\"aws\", region=\"us-west-2\"\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then we initialize the index. We will be using OpenAI's `text-embedding-ada-002` model for creating the embeddings, so we set the `dimension` to `1536`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'dimension': 1536,\n",
       " 'index_fullness': 0.0,\n",
       " 'namespaces': {},\n",
       " 'total_vector_count': 0}"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import time\n",
    "\n",
    "index_name = 'llama-2-rag'\n",
    "existing_indexes = [\n",
    "    index_info[\"name\"] for index_info in pc.list_indexes()\n",
    "]\n",
    "\n",
    "# check if index already exists (it shouldn't if this is first time)\n",
    "if index_name not in existing_indexes:\n",
    "    # if does not exist, create index\n",
    "    pc.create_index(\n",
    "        index_name,\n",
    "        dimension=1536,  # dimensionality of ada 002\n",
    "        metric='dotproduct',\n",
    "        spec=spec\n",
    "    )\n",
    "    # wait for index to be initialized\n",
    "    while not pc.describe_index(index_name).status['ready']:\n",
    "        time.sleep(1)\n",
    "\n",
    "# connect to index\n",
    "index = pc.Index(index_name)\n",
    "time.sleep(1)\n",
    "# view index stats\n",
    "index.describe_index_stats()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our index is now ready but it's empty. It is a vector index, so it needs vectors. As mentioned, to create these vector embeddings we will OpenAI's `text-embedding-ada-002` model — we can access it via LangChain like so:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.embeddings.openai import OpenAIEmbeddings\n",
    "\n",
    "embed_model = OpenAIEmbeddings(model=\"text-embedding-ada-002\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using this model we can create embeddings like so:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(2, 1536)"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "texts = [\n",
    "    'this is the first chunk of text',\n",
    "    'then another second chunk of text is here'\n",
    "]\n",
    "\n",
    "res = embed_model.embed_documents(texts)\n",
    "len(res), len(res[0])"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "From this we get two (aligning to our two chunks of text) 1536-dimensional embeddings.\n",
    "\n",
    "We're now ready to embed and index all our our data! We do this by looping through our dataset and embedding and inserting everything in batches."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "26087e3b0d2e4b8cb7c3e9fb12dc92a7",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/49 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from tqdm.auto import tqdm  # for progress bar\n",
    "\n",
    "data = dataset.to_pandas()  # this makes it easier to iterate over the dataset\n",
    "\n",
    "batch_size = 100\n",
    "\n",
    "for i in tqdm(range(0, len(data), batch_size)):\n",
    "    i_end = min(len(data), i+batch_size)\n",
    "    # get batch of data\n",
    "    batch = data.iloc[i:i_end]\n",
    "    # generate unique ids for each chunk\n",
    "    ids = [f\"{x['doi']}-{x['chunk-id']}\" for i, x in batch.iterrows()]\n",
    "    # get text to embed\n",
    "    texts = [x['chunk'] for _, x in batch.iterrows()]\n",
    "    # embed text\n",
    "    embeds = embed_model.embed_documents(texts)\n",
    "    # get metadata to store in Pinecone\n",
    "    metadata = [\n",
    "        {'text': x['chunk'],\n",
    "         'source': x['source'],\n",
    "         'title': x['title']} for i, x in batch.iterrows()\n",
    "    ]\n",
    "    # add to Pinecone\n",
    "    index.upsert(vectors=zip(ids, embeds, metadata))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can check that the vector index has been populated using `describe_index_stats` like before:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'dimension': 1536,\n",
       " 'index_fullness': 0.0,\n",
       " 'namespaces': {'': {'vector_count': 4838}},\n",
       " 'total_vector_count': 4838}"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "index.describe_index_stats()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Retrieval Augmented Generation"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We've built a fully-fledged knowledge base. Now it's time to connect that knowledge base to our chatbot. To do that we'll be diving back into LangChain and reusing our template prompt from earlier."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To use LangChain here we need to load the LangChain abstraction for a vector index, called a `vectorstore`. We pass in our vector `index` to initialize the object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/jamesbriggs/opt/anaconda3/envs/ml/lib/python3.9/site-packages/langchain_community/vectorstores/pinecone.py:74: UserWarning: Passing in `embedding` as a Callable is deprecated. Please pass in an Embeddings object instead.\n",
      "  warnings.warn(\n"
     ]
    }
   ],
   "source": [
    "from langchain.vectorstores import Pinecone\n",
    "\n",
    "text_field = \"text\"  # the metadata field that contains our text\n",
    "\n",
    "# initialize the vector store object\n",
    "vectorstore = Pinecone(\n",
    "    index, embed_model.embed_query, text_field\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using this `vectorstore` we can already query the index and see if we have any relevant information given our question about Llama 2."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Document(page_content='Alan Schelten Ruan Silva Eric Michael Smith Ranjan Subramanian Xiaoqing Ellen Tan Binh Tang\\nRoss Taylor Adina Williams Jian Xiang Kuan Puxin Xu Zheng Yan Iliyan Zarov Yuchen Zhang\\nAngela Fan Melanie Kambadur Sharan Narang Aurelien Rodriguez Robert Stojnic\\nSergey Edunov Thomas Scialom\\x03\\nGenAI, Meta\\nAbstract\\nIn this work, we develop and release Llama 2, a collection of pretrained and ﬁne-tuned\\nlarge language models (LLMs) ranging in scale from 7 billion to 70 billion parameters.\\nOur ﬁne-tuned LLMs, called L/l.sc/a.sc/m.sc/a.sc /two.taboldstyle-C/h.sc/a.sc/t.sc , are optimized for dialogue use cases. Our\\nmodels outperform open-source chat models on most benchmarks we tested, and based on\\nourhumanevaluationsforhelpfulnessandsafety,maybeasuitablesubstituteforclosedsource models. We provide a detailed description of our approach to ﬁne-tuning and safety', metadata={'source': 'http://arxiv.org/pdf/2307.09288', 'title': 'Llama 2: Open Foundation and Fine-Tuned Chat Models'}),\n",
       " Document(page_content='asChatGPT,BARD,andClaude. TheseclosedproductLLMsareheavilyﬁne-tunedtoalignwithhuman\\npreferences, which greatly enhances their usability and safety. This step can require signiﬁcant costs in\\ncomputeandhumanannotation,andisoftennottransparentoreasilyreproducible,limitingprogresswithin\\nthe community to advance AI alignment research.\\nIn this work, we develop and release Llama 2, a family of pretrained and ﬁne-tuned LLMs, L/l.sc/a.sc/m.sc/a.sc /two.taboldstyle and\\nL/l.sc/a.sc/m.sc/a.sc /two.taboldstyle-C/h.sc/a.sc/t.sc , at scales up to 70B parameters. On the series of helpfulness and safety benchmarks we tested,\\nL/l.sc/a.sc/m.sc/a.sc /two.taboldstyle-C/h.sc/a.sc/t.sc models generally perform better than existing open-source models. They also appear to\\nbe on par with some of the closed-source models, at least on the human evaluations we performed (see', metadata={'source': 'http://arxiv.org/pdf/2307.09288', 'title': 'Llama 2: Open Foundation and Fine-Tuned Chat Models'}),\n",
       " Document(page_content='Baptiste Rozière, Naman Goyal, Eric Hambro, Faisal Azhar, Aur’elien Rodriguez, Armand Joulin, Edouard\\nGrave, and Guillaume Lample. Llama: Open and eﬃcient foundation language models. arXiv preprint\\narXiv:2302.13971 , 2023.\\nAshish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser,\\nand Illia Polosukhin. Attention is all you need, 2017.\\nOriol Vinyals, Igor Babuschkin, Wojciech M Czarnecki, Michaël Mathieu, Andrew Dudzik, Junyoung Chung,\\nDavid H Choi, Richard Powell, Timo Ewalds, Petko Georgiev, et al. Grandmaster level in starcraft ii using\\nmulti-agent reinforcement learning. Nature, 575(7782):350–354, 2019.\\nYizhong Wang, Yeganeh Kordi, Swaroop Mishra, Alisa Liu, Noah A Smith, Daniel Khashabi, and HannanehHajishirzi. Self-instruct: Aligninglanguagemodel withselfgeneratedinstructions. arXivpreprint', metadata={'source': 'http://arxiv.org/pdf/2307.09288', 'title': 'Llama 2: Open Foundation and Fine-Tuned Chat Models'})]"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "query = \"What is so special about Llama 2?\"\n",
    "\n",
    "vectorstore.similarity_search(query, k=3)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We return a lot of text here and it's not that clear what we need or what is relevant. Fortunately, our LLM will be able to parse this information much faster than us. All we need is to connect the output from our `vectorstore` to our `chat` chatbot. To do that we can use the same logic as we used earlier."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "def augment_prompt(query: str):\n",
    "    # get top 3 results from knowledge base\n",
    "    results = vectorstore.similarity_search(query, k=3)\n",
    "    # get the text from the results\n",
    "    source_knowledge = \"\\n\".join([x.page_content for x in results])\n",
    "    # feed into an augmented prompt\n",
    "    augmented_prompt = f\"\"\"Using the contexts below, answer the query.\n",
    "\n",
    "    Contexts:\n",
    "    {source_knowledge}\n",
    "\n",
    "    Query: {query}\"\"\"\n",
    "    return augmented_prompt"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using this we produce an augmented prompt:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Using the contexts below, answer the query.\n",
      "\n",
      "    Contexts:\n",
      "    Alan Schelten Ruan Silva Eric Michael Smith Ranjan Subramanian Xiaoqing Ellen Tan Binh Tang\n",
      "Ross Taylor Adina Williams Jian Xiang Kuan Puxin Xu Zheng Yan Iliyan Zarov Yuchen Zhang\n",
      "Angela Fan Melanie Kambadur Sharan Narang Aurelien Rodriguez Robert Stojnic\n",
      "Sergey Edunov Thomas Scialom\u0003\n",
      "GenAI, Meta\n",
      "Abstract\n",
      "In this work, we develop and release Llama 2, a collection of pretrained and ﬁne-tuned\n",
      "large language models (LLMs) ranging in scale from 7 billion to 70 billion parameters.\n",
      "Our ﬁne-tuned LLMs, called L/l.sc/a.sc/m.sc/a.sc /two.taboldstyle-C/h.sc/a.sc/t.sc , are optimized for dialogue use cases. Our\n",
      "models outperform open-source chat models on most benchmarks we tested, and based on\n",
      "ourhumanevaluationsforhelpfulnessandsafety,maybeasuitablesubstituteforclosedsource models. We provide a detailed description of our approach to ﬁne-tuning and safety\n",
      "asChatGPT,BARD,andClaude. TheseclosedproductLLMsareheavilyﬁne-tunedtoalignwithhuman\n",
      "preferences, which greatly enhances their usability and safety. This step can require signiﬁcant costs in\n",
      "computeandhumanannotation,andisoftennottransparentoreasilyreproducible,limitingprogresswithin\n",
      "the community to advance AI alignment research.\n",
      "In this work, we develop and release Llama 2, a family of pretrained and ﬁne-tuned LLMs, L/l.sc/a.sc/m.sc/a.sc /two.taboldstyle and\n",
      "L/l.sc/a.sc/m.sc/a.sc /two.taboldstyle-C/h.sc/a.sc/t.sc , at scales up to 70B parameters. On the series of helpfulness and safety benchmarks we tested,\n",
      "L/l.sc/a.sc/m.sc/a.sc /two.taboldstyle-C/h.sc/a.sc/t.sc models generally perform better than existing open-source models. They also appear to\n",
      "be on par with some of the closed-source models, at least on the human evaluations we performed (see\n",
      "Baptiste Rozière, Naman Goyal, Eric Hambro, Faisal Azhar, Aur’elien Rodriguez, Armand Joulin, Edouard\n",
      "Grave, and Guillaume Lample. Llama: Open and eﬃcient foundation language models. arXiv preprint\n",
      "arXiv:2302.13971 , 2023.\n",
      "Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser,\n",
      "and Illia Polosukhin. Attention is all you need, 2017.\n",
      "Oriol Vinyals, Igor Babuschkin, Wojciech M Czarnecki, Michaël Mathieu, Andrew Dudzik, Junyoung Chung,\n",
      "David H Choi, Richard Powell, Timo Ewalds, Petko Georgiev, et al. Grandmaster level in starcraft ii using\n",
      "multi-agent reinforcement learning. Nature, 575(7782):350–354, 2019.\n",
      "Yizhong Wang, Yeganeh Kordi, Swaroop Mishra, Alisa Liu, Noah A Smith, Daniel Khashabi, and HannanehHajishirzi. Self-instruct: Aligninglanguagemodel withselfgeneratedinstructions. arXivpreprint\n",
      "\n",
      "    Query: What is so special about Llama 2?\n"
     ]
    }
   ],
   "source": [
    "print(augment_prompt(query))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There is still a lot of text here, so let's pass it onto our chat model to see how it performs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Llama 2 is a collection of pretrained and fine-tuned large language models (LLMs) developed in a work. These LLMs range in scale from 7 billion to 70 billion parameters. They are optimized specifically for dialogue use cases.\n",
      "\n",
      "What makes Llama 2 special is that its fine-tuned LLMs, such as L/l.sc/a.sc/m.sc/a.sc /two.taboldstyle-C/h.sc/a.sc/t.sc, have shown better performance compared to existing open-source chat models on various benchmarks. They have also been evaluated in terms of helpfulness and safety, and they appear to be on par with some closed-source models.\n",
      "\n",
      "This work aims to provide a detailed description of the approach used for fine-tuning and ensuring safety in Llama 2, similar to other closed-source LLMs like ChatGPT, BARD, and Claude. The closed-source LLMs are extensively fine-tuned to align with human preferences, enhancing their usability and safety. However, this fine-tuning process often involves significant costs in terms of computation and human annotation, and it is not always transparent or easily reproducible, which can limit progress in AI alignment research within the community.\n",
      "\n",
      "In summary, Llama 2 offers pretrained and fine-tuned LLMs optimized for dialogue use cases, outperforming open-source chat models on benchmarks and potentially serving as substitutes for closed-source models in terms of helpfulness and safety.\n"
     ]
    }
   ],
   "source": [
    "# create a new user prompt\n",
    "prompt = HumanMessage(\n",
    "    content=augment_prompt(query)\n",
    ")\n",
    "# add to messages\n",
    "messages.append(prompt)\n",
    "\n",
    "res = chat(messages)\n",
    "\n",
    "print(res.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can continue with more Llama 2 questions. Let's try _without_ RAG first:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "In the provided context, it is mentioned that Llama 2, the collection of pretrained and fine-tuned large language models (LLMs), includes safety measures in its development. However, the specific safety measures used in the development of Llama 2 are not detailed in the given text. It only mentions that safety is a consideration in their approach to fine-tuning and that closed-source models are heavily fine-tuned to align with human preferences, enhancing their usability and safety.\n",
      "\n",
      "To obtain more specific information regarding the safety measures implemented in the development of Llama 2, it would be necessary to refer to the original research paper or documentation related to Llama 2.\n"
     ]
    }
   ],
   "source": [
    "prompt = HumanMessage(\n",
    "    content=\"what safety measures were used in the development of llama 2?\"\n",
    ")\n",
    "\n",
    "res = chat(messages + [prompt])\n",
    "print(res.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The chatbot is able to respond about Llama 2 thanks to it's conversational history stored in `messages`. However, it doesn't know anything about the safety measures themselves as we have not provided it with that information via the RAG pipeline. Let's try again but with RAG."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Based on the provided contexts, the development of Llama 2 involved several safety measures. The authors of Llama 2 took steps to increase the safety of the models by using safety-specific data annotation and tuning. They also conducted red-teaming and employed iterative evaluations to ensure safety. These measures were implemented to reduce potential risks and enhance the safety of the Llama 2 models.\n",
      "\n",
      "The authors of Llama 2 also emphasized the importance of improving the safety of language models in their work. They provided a detailed description of their approach to fine-tuning and safety, aiming to contribute to the responsible development of large language models (LLMs). By sharing their methodology and observations, they hope to enable the community to reproduce fine-tuned LLMs and continue to enhance their safety.\n",
      "\n",
      "It is worth noting that the specific details of the safety measures used in the development of Llama 2 may require further examination of the referenced papers and resources.\n"
     ]
    }
   ],
   "source": [
    "prompt = HumanMessage(\n",
    "    content=augment_prompt(\n",
    "        \"what safety measures were used in the development of llama 2?\"\n",
    "    )\n",
    ")\n",
    "\n",
    "res = chat(messages + [prompt])\n",
    "print(res.content)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We get a much more informed response that includes several items missing in the previous non-RAG response, such as \"red-teaming\", \"iterative evaluations\", and the intention of the researchers to share this research to help \"improve their safety, promoting responsible development in the field\"."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Delete the index to save resources:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pc.delete_index(index_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "redacre",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.12"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
